package br.edu.ufcg.greengrid;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;

import br.edu.ufcg.greengrid.event.JobSubmissionEvent;
import br.edu.ufcg.greengrid.event.MachineAvailabilityEvent;
import br.edu.ufcg.greengrid.event.MachineIdToOperationsPerSecondEvent;
import br.edu.ufcg.greengrid.event.reader.JobSubmissionEventReader;
import br.edu.ufcg.greengrid.event.reader.MachineAvailabilityEventReader;
import br.edu.ufcg.greengrid.event.reader.MachineIdToOperationsPerSecondEventReader;
import br.edu.ufcg.greengrid.model.Grid;
import br.edu.ufcg.greengrid.model.Simulator;
import br.edu.ufcg.greengrid.model.job.Job;
import br.edu.ufcg.greengrid.model.job.JobFactory;
import br.edu.ufcg.greengrid.model.job.JobFactoryImpl;
import br.edu.ufcg.greengrid.model.job.JobWithCheckpointFactoryImpl;
import br.edu.ufcg.greengrid.model.job.scheduler.DummyJobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.FastestMachineJobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.JobScheduler;
import br.edu.ufcg.greengrid.model.job.scheduler.SlowestMachineJobScheduler;
import br.edu.ufcg.greengrid.model.machine.state.MachineState.MSEnum;
import br.edu.ufcg.greengrid.model.machine.state.factory.MachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.NormalMachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.SleepMachineStateFactory;
import br.edu.ufcg.greengrid.model.machine.state.factory.StandByMachineStateFactory;
import br.edu.ufcg.greengrid.report.JobMakespanReporter;
import br.edu.ufcg.greengrid.report.MachineStateReporter;
import br.edu.ufcg.greengrid.report.Reporter;
import br.edu.ufcg.greengrid.report.SummaryReporter;

public class Main {

	private static final long DEFAULT_TIME_TO_OPERATIONS_FACTOR = Job.DEFAULT_TIME_TO_OPERATIONS_FACTOR;

	private static final String OPT_MACHINE_AVAILABILITY_WORKLOAD = "MAW";
	private static final String OPT_JOB_SUBMISSION_WORKLOAD = "JSW";
	private static final String OPT_DELAY_FIRST_JOB = "DFIMFJ";
	private static final String OPT_GREEN_EXECUTION_MODE = "GEM";
	private static final String OPT_GREEN_EXECUTION_MODE_SLEEP = "SLEEP";
	private static final String OPT_GREEN_EXECUTION_MODE_STANDBY = "STANDBY";
	private static final String OPT_GREEN_EXECUTION_MODE_OFF = "OFF";
	private static final String OPT_CHECKPOINT = "C";
	private static final String OPT_ON = "ON";
	private static final String OPT_OFF = "OFF";
	private static final String OPT_TIME_TO_OPERATIONS_FACTOR = "T2O";
	private static final String OPT_MACHINE_ID_TO_OPERATIONS_PER_SECOND = "M2O";
	private static final String OPT_JOB_SCHEDULER = "JS";
	private static final String OPT_JOB_SCHEDULER_DUMMY = "DUMMY";
	private static final String OPT_JOB_SCHEDULER_FASTEST = "FASTEST";
	private static final String OPT_JOB_SCHEDULER_SLOWEST = "SLOWEST";
	private static final String OPT_SUMMARY_REPORTER = "SUMMARYR";
	private static final String OPT_MAX_JOB_FAILURES = "MJF";

	@SuppressWarnings("static-access")
	private static Options getMainOptions() {

		Option machineAvailabilityWorkload = OptionBuilder.withDescription(
				"The file which has the machine availability workload").withLongOpt("machineAvailabilityWorkload")
				.hasArg().withArgName("file name").isRequired().create(OPT_MACHINE_AVAILABILITY_WORKLOAD);
		Option jobSubmissionWorkload = OptionBuilder.withDescription("The file which has the job submission workload")
				.withLongOpt("jobSubmissionWorkload").hasArg().withArgName("file name").isRequired().create(
						OPT_JOB_SUBMISSION_WORKLOAD);
		Option delayBetweenFirstIdleMachineAndFirstJob = OptionBuilder.withDescription(
				"The delay between the first idle machine and the first job").withLongOpt(
				"delayBetweenFirstIdleMachineAndFirstJob").hasArg().withArgName("delay").create(OPT_DELAY_FIRST_JOB);
		Option greenExecutionMode = OptionBuilder.withDescription(
				"The green execution mode of the simulation: " + OPT_GREEN_EXECUTION_MODE_OFF + ", "
						+ OPT_GREEN_EXECUTION_MODE_SLEEP + ", " + OPT_GREEN_EXECUTION_MODE_STANDBY).withLongOpt(
				"greenExecutionMode").hasArg().withArgName("mode").isRequired().create(OPT_GREEN_EXECUTION_MODE);
		Option checkpoint = OptionBuilder.withDescription("Using checkpoint?").withLongOpt("checkpoint").hasArgs()
				.withArgName(OPT_ON + "/" + OPT_OFF).isRequired().create(OPT_CHECKPOINT);
		Option timeToOperations = OptionBuilder.withDescription(
				"The factor that converts one second of a job in number of operations.")
				.withLongOpt("timeToOperations").hasArg().withArgName("factor").create(OPT_TIME_TO_OPERATIONS_FACTOR);
		Option machineIdToOperationsPerSecond = OptionBuilder.withDescription(
				"The file which has the machine id to operations per second info").withLongOpt(
				"machineIdToOperationsPerSecond").hasArg().withArgName("file name").create(
				OPT_MACHINE_ID_TO_OPERATIONS_PER_SECOND);
		Option scheduler = OptionBuilder.withDescription("Job scheduler").withLongOpt("jobScheduler").hasArgs()
				.withArgName(
						OPT_JOB_SCHEDULER_DUMMY + "/" + OPT_JOB_SCHEDULER_FASTEST + "/" + OPT_JOB_SCHEDULER_SLOWEST)
				.create(OPT_JOB_SCHEDULER);
		Option summaryr = OptionBuilder.withDescription("Turn on Summary reporter?").withLongOpt("summaryReporter")
				.hasArgs().withArgName(OPT_ON + "/" + OPT_OFF).create(OPT_SUMMARY_REPORTER);
		Option maxJobFailures = OptionBuilder.withDescription("The maximum number of failures a job may have")
				.withLongOpt("maxJobFailures").hasArg().withArgName("n").create(OPT_MAX_JOB_FAILURES);

		Options options = new Options();
		options.addOption(machineAvailabilityWorkload);
		options.addOption(jobSubmissionWorkload);
		options.addOption(delayBetweenFirstIdleMachineAndFirstJob);
		options.addOption(greenExecutionMode);
		options.addOption(checkpoint);
		options.addOption(timeToOperations);
		options.addOption(machineIdToOperationsPerSecond);
		options.addOption(scheduler);
		options.addOption(summaryr);
		options.addOption(maxJobFailures);

		return options;
	}

	private static void printUsage(Options options) {

		HelpFormatter hf = new HelpFormatter();
		hf.printHelp(120, "Greengrid-Simulator", null, options, null, true);
	}

	private static void error(Options options) {

		printUsage(options);
		System.exit(1);
	}

	public static void main(String[] args) throws IOException {

		CommandLineParser parser = new PosixParser();
		CommandLine commandLine = null;
		Options options = null;

		try {
			options = getMainOptions();
			commandLine = parser.parse(options, args);
			String[] leftArgs = commandLine.getArgs();

			if (leftArgs.length != 0) {
				System.out.println("Error! Unknown arguments: " + Arrays.toString(leftArgs));
				error(options);
			}
		} catch (ParseException e1) {
			error(options);
		}

		File machineAvailabilityWorkloadFileName = new File(commandLine
				.getOptionValue(OPT_MACHINE_AVAILABILITY_WORKLOAD));
		File jobSubmissionWorkloadFileName = new File(commandLine.getOptionValue(OPT_JOB_SUBMISSION_WORKLOAD));
		Long delta = null;
		if (commandLine.hasOption(OPT_DELAY_FIRST_JOB)) {
			delta = Long.parseLong(commandLine.getOptionValue(OPT_DELAY_FIRST_JOB));
		}

		MachineStateFactory msf = null;
		String greenExecutionModeValue = commandLine.getOptionValue(OPT_GREEN_EXECUTION_MODE);
		if (greenExecutionModeValue.equals(OPT_GREEN_EXECUTION_MODE_OFF)) {
			msf = new NormalMachineStateFactory();
		} else if (greenExecutionModeValue.equals(OPT_GREEN_EXECUTION_MODE_SLEEP)) {
			msf = new SleepMachineStateFactory();
		} else if (greenExecutionModeValue.equals(OPT_GREEN_EXECUTION_MODE_STANDBY)) {
			msf = new StandByMachineStateFactory();
		} else {
			throw new IllegalArgumentException(String.format("Unknown value for green execution mode: %s",
					greenExecutionModeValue));
		}

		Map<String, Long> machineIdToOperationsPerSecond = Grid.DEFAULT_MACHINE_ID_TO_OPERATIONS_PER_SECOND;
		String machineIdToOperationsPerSecondValue = commandLine
				.getOptionValue(OPT_MACHINE_ID_TO_OPERATIONS_PER_SECOND);
		if (machineIdToOperationsPerSecondValue != null) {
			List<MachineIdToOperationsPerSecondEvent> machineIdToOperationsPerSecondList = new MachineIdToOperationsPerSecondEventReader()
					.read(new File(machineIdToOperationsPerSecondValue));
			machineIdToOperationsPerSecond = new HashMap<String, Long>();
			for (MachineIdToOperationsPerSecondEvent entry : machineIdToOperationsPerSecondList) {
				machineIdToOperationsPerSecond.put(entry.getMachineId(), entry.getOperationsPerSecond());
			}
		}

		JobScheduler scheduler = null;
		String jobSchedulerValue = commandLine.getOptionValue(OPT_JOB_SCHEDULER);
		if (jobSchedulerValue == null) {
			scheduler = Grid.DEFAULT_JOB_SCHEDULER;
		} else if (jobSchedulerValue.equals(OPT_JOB_SCHEDULER_DUMMY)) {
			scheduler = new DummyJobScheduler();
		} else if (jobSchedulerValue.equals(OPT_JOB_SCHEDULER_SLOWEST)) {
			scheduler = new SlowestMachineJobScheduler();
		} else if (jobSchedulerValue.equals(OPT_JOB_SCHEDULER_FASTEST)) {
			scheduler = new FastestMachineJobScheduler();
		}

		long maxJobFailures = Grid.DEFAULT_MAX_JOB_FAILURES;
		String maxJobFailuresValue = commandLine.getOptionValue(OPT_MAX_JOB_FAILURES);
		if (maxJobFailuresValue != null){
			maxJobFailures = Long.parseLong(maxJobFailuresValue);
		}
		
		Grid grid = new Grid(msf, machineIdToOperationsPerSecond, scheduler, maxJobFailures);
		List<MachineAvailabilityEvent> machineAvailabilityWorkload = new MachineAvailabilityEventReader()
				.read(machineAvailabilityWorkloadFileName);
		if (machineAvailabilityWorkload.isEmpty()) {
			throw new IllegalArgumentException(String.format(
					"Error! There is no machine availability event in file: %s", machineAvailabilityWorkloadFileName));
		}
		Long clockStartTime = machineAvailabilityWorkload.get(0).getTimestamp();
		List<JobSubmissionEvent> gridWorkload = new JobSubmissionEventReader(clockStartTime, delta)
				.read(jobSubmissionWorkloadFileName);

		long timeToOperations = DEFAULT_TIME_TO_OPERATIONS_FACTOR;
		String timeToOperationsValue = commandLine.getOptionValue(OPT_TIME_TO_OPERATIONS_FACTOR);
		if (timeToOperationsValue != null) {
			timeToOperations = Long.parseLong(timeToOperationsValue);
		}

		
		JobFactory jobFactory = null;
		String checkpointValue = commandLine.getOptionValue(OPT_CHECKPOINT);
		if (checkpointValue.equals(OPT_ON)) {
			jobFactory = new JobWithCheckpointFactoryImpl(timeToOperations);
		} else if (checkpointValue.equals(OPT_OFF)) {
			jobFactory = new JobFactoryImpl(timeToOperations);
		} else {
			throw new IllegalArgumentException(String.format("Unknown value for checkpoint: %s", checkpointValue));
		}

		Simulator s = new Simulator(grid, machineAvailabilityWorkload, gridWorkload, jobFactory);

		boolean summaryr = true;
		String summaryrValue = commandLine.getOptionValue(OPT_SUMMARY_REPORTER);
		if (summaryrValue != null && summaryrValue.equals(OPT_OFF)) {
			summaryr = false;
		}

		long randomValue = new Random().nextLong();
		List<Reporter> reporters = new LinkedList<Reporter>();
		if (summaryr) {
			reporters.add(new SummaryReporter(randomValue));
		}
		reporters.add(new JobMakespanReporter(randomValue));
		reporters.add(new MachineStateReporter(randomValue));

		while (!s.done()) {
			s.nextStep();
			for (Reporter r : reporters) {
				r.handleNewTimestamp(s);
			}
//			checkInvariants(s);
		}

		for (Reporter r : reporters) {
			r.cleanUp(s);
			r.close(s);
			r.generateOutput();
		}

	}

	private static void checkInvariants(Simulator s) {

		Grid g = s.getGrid();
		boolean hasWaitingJobs = g.getWaitingJobs().size() != 0;
		int jobsInPool = g.getWaitingJobs().size();
		int nmachinesInGoingGreenState = g.getMachines(MSEnum.GOING_GREEN).size();
		int nmachinesInGreenState = g.getMachines(MSEnum.GREEN).size();
		int nmachinesInLeavingGreenState = g.getMachines(MSEnum.LEAVING_GREEN).size();
		int nmachinesInIdleState = g.getMachines(MSEnum.IDLE).size();
		boolean hasMachinesInGreenState = nmachinesInGreenState != 0;

		if(hasWaitingJobs && hasMachinesInGreenState && (nmachinesInLeavingGreenState + nmachinesInIdleState < jobsInPool)){
			throw new RuntimeException(String.format("Invariant broken at clock: %d!\nThe grid has jobs in the pool but there are machines in GREEN state: %d\nnmachinesInGoingGreenState: %d\nnmachinesInLeavingGreenState: %d\nnmachinesInIdleState: %d\njobsInPool: %d", s.getClock(), nmachinesInGreenState, nmachinesInGoingGreenState, nmachinesInLeavingGreenState, nmachinesInIdleState, jobsInPool));
		}

	}
}
